Tomcat 查询和拉取镜像 Tomcat 的 Docker Hub 下载地址 ,对于初学者来说,Tomcat 镜像的 Tag 命名看起来像一串乱码(例如 9.0.118-jdk17-corretto-al2),但其实它遵循了极其严谨的“组合式命名规范”。 这串名字是由 4 个核心部分拼接而成的,就像公式一样:[Tomcat版本]-[JDK版本]-[JDK发行版]-[底层操作系统]。如果是生产环境上线,强烈建议选择带有精确三位版本号的标签,例如 9.0.118-jdk17-corretto。这样可以保证无论何时自动化构建(CI/CD)拉取镜像,代码环境都保持绝对一致,避免因为 Tomcat 偷偷升级导致应用崩溃。
第一部分:Tomcat 版本(核心中间件)。9.0.118 (具体三位版本号):最精确的生产级版本。推荐在生产环境使用,能死死锁住版本,防止意外升级。9.0 (两位版本号):动态指针,永远指向 9.0.x 系列中的最新版。9 (主版本号):动态指针,指向大版本 9.x.x 的最新版。
第二部分:Java / JDK 版本(运行环境)。对应名字中的 jdk8、jdk11、jdk17、jdk21、jre25 等。jdk17 说明容器内安装的是 JDK 17(包含完整的 Java 开发工具包和编译器),jre25 说明内嵌的是 JRE 25(仅包含运行时环境,体积更小,安全性更高,因为去掉了编译工具)。
第三部分:JDK 发行版(谁家提供的 Java)。对应名字中的 corretto、temurin 等,由于 Oracle JDK 商业化策略的调整,目前主流镜像都使用开源的 OpenJDK 衍生版。corretto (亚马逊提供),这是亚马逊官方维护的免费、多平台、生产就绪的 OpenJDK 发行版。它在云原生和 AWS 生态中性能极佳,长久稳定支持。temurin (Eclipse 基金会提供): Eclipse Temurin(原名 AdoptOpenJDK)。目前非常主流、极为纯净、完全开源的 Java 运行时,广泛用于各大企业的标准环境。
第四部分:底层操作系统(洋葱模型的底座)。对应名字末尾的 al2、noble,或者有些省略不写的。-al2 (Amazon Linux 2) 说明这个镜像的最底层操作系统是亚马逊基于 RHEL(红帽)定制的 Amazon Linux 2 操作系统(在使用习惯上类似于 CentOS/RHEL)。-noble (Ubuntu 24.04) 说明底层操作系统是 Ubuntu 24.04 LTS (代号 Noble Numbat)。如果名字到 corretto 就戛然而止了(例如 9.0-jdk17-corretto),它默认通常会使用标准的 Debian 或标准的 Linux 基础层。
1 $ docker pull tomcat:9.0.118-jdk17-corretto
创建容器实例 1 2 3 4 5 6 7 8 9 10 11 12 13 $ docker run -d -p 8080:8080 --name=tomcat01 tomcat:9.0.118-jdk17-corretto $ docker -it 8d5d3164637e /bib/bash $$ cd /usr/local/tomcat $$ ls -a $$ rm -rf webapps $$ mv webapps.dist webapps curl http://192.168.1.8:8080
MySQL 基础安装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 $ docker pull mysql:8.4.9-oraclelinux9 $ docker run \ -p 3306:3306 \ --privileged=true \ -v /opt/apps/mysql/data:/var/lib/mysql \ -v /opt/apps/mysql/conf:/etc/mysql/conf.d \ -v /opt/apps/mysql/log:/var/log \ --name=mysql01 \ -e MYSQL_ROOT_PASSWORD=123456 \ -d mysql:8.4.9-oraclelinux9 \ --mysql-native-password=ON $ docker ps $ docker exec -it 51bc3f102318 /bin/bash $$ mysql -uroot -p $$$ show databases; $$$ create database db01; $$$ use db01; $$$ create table t1(id int not null primary key auto_increment, name varchar(20)); $$$ insert into t1(id , name) values(1, '张三' ); $$$ SELECT user, host, plugin FROM mysql.user; $$$ ALTER USER 'root' @'%' IDENTIFIED WITH mysql_native_password BY '123456' ;
更改编码设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 $$$ show variables like 'character%' ; +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | latin1 | | character_set_connection | latin1 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | latin1 | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.4/charsets/ | +--------------------------+--------------------------------+ $ /opt/apps/mysql/conf$ ls $ /opt/apps/mysql/conf$ vim my.cnf [client] default_character_set=utf8 [mysqld] collation_server=utf8_general_ci character_set_server=utf8 $ docker restart mysql01 $$$ show variables like 'character%' ; +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb3 | | character_set_connection | utf8mb3 | | character_set_database | utf8mb3 | | character_set_filesystem | binary | | character_set_results | utf8mb3 | | character_set_server | utf8mb3 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.4/charsets/ | +--------------------------+--------------------------------+ $$$ create database db02; $$$ use db02; $$$ create table t1(id int not null primary key auto_increment, name varchar(20)); $$$ insert into t1(id , name) values(1, '张三' ); $$$ select * from t1 limit 0,10;
主从复制 更改主服务器配置。在宿主机编辑配置文件(master配置文件): /opt/apps/mysql/conf/my.cnf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 [client] default_character_set=utf8 [mysqld] collation_server=utf8_general_ci character_set_server=utf8 server_id=101 binlog-ignore-db=mysql log-bin=mall-mysql-bin binlog_cache_size=1M binlog_format=mixed binlog_expire_logs_seconds=604800 slave_skip_errors=1062
重启master实例:
1 2 $ docker restart mysql01 $ docker ps
登录数据库,创建数据同步的用户
1 2 3 4 5 6 $ docker exec -it 51bc3f102318 /bin/bash $$ mysql -uroot -p $$$ show databases; $$$ create user 'slave' @'%' identified by '123456' ; $$$ grant replication slave, replication client on *.* to 'slave' @'%' ;
退出到宿主机,创建从服务器 3308
1 2 3 4 5 6 7 8 9 10 $ docker run \ -p 3308:3306 \ --privileged=true \ -v /opt/apps/mysql-slave/data:/var/lib/mysql \ -v /opt/apps/mysql-slave/conf:/etc/mysql/conf.d \ -v /opt/apps/mysql-slave/log:/var/log \ --name=mysql02 \ -e MYSQL_ROOT_PASSWORD=123456 \ -d mysql:8.4.9-oraclelinux9 \ --mysql-native-password=ON
编辑从服务器的配置文件 /opt/apps/mysql-slave/conf/my.cnf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 [client] default_character_set=utf8 [mysqld] collation_server=utf8_general_ci character_set_server=utf8 server_id=102 binlog-ignore-db=mysql log-bin=mall-mysql-slave1-bin binlog_cache_size=1M binlog_format=mixed binlog_expire_logs_seconds=604800 slave_skip_errors=1062 relay_log=mall-mysql-relay-bin log_replica_updates=1 read_only=1 super_read_only=1
重启slave实例:
1 2 $ docker restart mysql02 $ docker ps
在从机上配置需要同步的主机服务器信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 $$$ SHOW BINARY LOG STATUS; +-----------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +-----------------------+----------+--------------+------------------+-------------------+ | mall-mysql-bin.000001 | 158 | | mysql | | +-----------------------+----------+--------------+------------------+-------------------+ $ docker exec -it 69e24e18efe2 mysql -uroot -p123456 $$$ CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.1.8' , SOURCE_USER='slave' , SOURCE_PASSWORD='123456' , SOURCE_PORT=3306, SOURCE_LOG_FILE='mall-mysql-bin.000001' , SOURCE_LOG_POS=158, SOURCE_CONNECT_RETRY=30; $$$ SHOW REPLICA STATUS\G; *************************** 1. row *************************** Replica_IO_State: Source_Host: 192.168.1.8 Source_User: slave Source_Port: 3306 Connect_Retry: 30 Source_Log_File: mall-mysql-bin.000001 Read_Source_Log_Pos: 158 Relay_Log_File: mall-mysql-relay-bin.000001 Relay_Log_Pos: 4 Relay_Source_Log_File: mall-mysql-bin.000001 Replica_IO_Running: No Replica_SQL_Running: No ... $$$ START REPLICA; $$$ SHOW REPLICA STATUS\G *************************** 1. row *************************** Replica_IO_State: Source_Host: 192.168.1.8 Source_User: slave Source_Port: 3306 Connect_Retry: 30 Source_Log_File: mall-mysql-bin.000001 Read_Source_Log_Pos: 158 Relay_Log_File: mall-mysql-relay-bin.000001 Relay_Log_Pos: 4 Relay_Source_Log_File: mall-mysql-bin.000001 Replica_IO_Running: No Replica_SQL_Running: Yes ... Last_IO_Error: Error connecting to source 'slave@192.168.1.8:3306' . This was attempt 10/10, with a delay of 30 seconds between attempts. Message: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection. ... $$$ STOP REPLICA; $$$ CHANGE REPLICATION SOURCE TO GET_SOURCE_PUBLIC_KEY=1; $$$ START REPLICA; $$$ SET GLOBAL super_read_only = 0; $$$ ALTER USER 'root' @'%' IDENTIFIED WITH mysql_native_password BY '123456' ; $$$ FLUSH PRIVILEGES; $$$ SET GLOBAL super_read_only = 1;
主从复制测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $$$ create database db03; $$$ show databases; $$$ create table t_user(id int not null primary key auto_increment, name varchar(20)); $$$ show tables; $$$ insert t_user(id ,name) values (2, 'zhangsan002' ); $$$ select * from t_user;
Redis 单机版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 $ docker pull redis:8.8.0 $ mkdir -p /opt/apps/redis/conf $ touch /opt/apps/redis/conf/redis.conf $ cat <<EOF > /opt/apps/redis/conf/redis.conf # 允许任意 IP 连接访问 bind 0.0.0.0 # 非守护模式启动 daemonize no # 关闭保护模式,允许外部网络访问 protected-mode no # 开启 AOF 持久化(强力数据保护) appendonly yes # 设置你的强 Redis 访问密码 requirepass 123456 EOF $ docker run -d \ -p 6379:6379 \ --name=redis01 \ -v /opt/apps/redis/conf/redis.conf:/etc/redis/redis.conf \ -v /opt/apps/redis/data:/data \ redis:8.8.0 \ redis-server /etc/redis/redis.conf \ $$ redis-cli $$ auth 123456 $$ set a 111 $$ get a "111"
集群版 哈希槽理论部分 下面通过哈希取余算法、一致性哈希算法、哈希槽算法的演进史,介绍实际生产中经常遇到的分布式系统分布式寻址、高可用、扩容缩容等核心问题。
哈希取余算法(Hash Modulo)—— 基础传统方案
核心原理:假设系统有 $N$ 台机器,当一个请求/数据进来时,先计算其 Key 的哈希值,然后直接对机器数量 $N$ 取余,通过计算结果,数据被直接路由到对应的机器上。
优点:简单粗暴,计算速度极快,数据在节点数量固定的情况下分布相对均匀。
致命痛点(扩缩容雪崩):一旦节点数量 $N$ 发生变化(增加或减少一台机器),所有数据的路由公式全变了!比如 $N$ 从 3 变成 4,原本 $\pmod 3$ 的数据大面积失效,导致旧数据几乎需要 100% 重新迁移(Rehash)。在缓存场景下,这会导致缓存大面积瞬间失效,引发缓存雪崩,流量直接冲垮后端数据库。
一致性哈希算法(Consistent Hashing)—— 革命性的圆环设计
核心原理:一致性哈希不再直接对节点数取余,而是引入了一个固定的 哈希环(大小为 $2^{32}$)。
节点映射:将每台服务器的 IP 或主机名进行哈希,计算出的结果对应到环上的某个位置。
数据映射:当数据 Key 进来时,同样计算哈希值映射到环上。
路由规则:从数据落点位置开始,顺时针寻找碰到的第一台服务器,该服务器即为数据的存储节点。
如何解决痛点(扩缩容优势):当新增或删除一个节点时,受影响的只有该节点在环上逆时针方向到前一个节点之间的一小段数据,其余绝大部分数据依然顺时针路由到原本的节点。数据迁移量从“全量”降到了“局部($\frac{1}{N}$)”。
哈希倾斜与虚拟节点(Virtual Nodes):
问题:如果服务器节点太少,它们在环上的分布可能不均匀,导致某些节点承载极其恐怖的流量(数据倾斜)。
解法:引入虚拟节点。把一台真实的物理机虚拟成几百个节点(如 NodeA-1, NodeA-2)散落在环上。虚拟节点越多,数据分布就越趋近于绝对均匀。
哈希槽算法(Hash Slots)—— 现代工业级落地(以 Redis Cluster 为代表)
核心原理:哈希槽(以 Redis 为例)将整个集群抽象地划分为固定数量的槽位(16384 个槽,即 0 - 16383)。
槽位分配:集群启动时,把这 16384 个槽平均或按性能手动分配给不同的物理节点(如 NodeA 负责 0-5000,NodeB 负责 5001-10000…)。
数据寻址:数据 Key 进来时,利用 CRC16 算法计算出哈希值,然后对固定的 16384 取余,得到一个槽号:$\text{Slot Index} = \text{CRC16(Key)} \pmod{16384}$
根据槽号,直接去找负责这个槽的服务器。
为什么可以替代一致性哈希?:
解耦了数据与节点的直接映射:在一致性哈希中,数据路由完全取决于节点在环上的物理位置,节点变动时,数据的迁移是伴随着底层哈希指针的改变被动发生的。 而哈希槽算法中,数据只和槽绑定,槽和节点绑定。
完美的精细化扩缩容控制(核心):当你想增加一台新机器时,你可以精准地从原本的机器里各自抽出一部分槽(比如各抽 1000 个槽)挪给新机器。这种槽的移动是完全由运维人员或自动化脚本 “人工控制” 的。在迁移过程中,Redis 还可以继续对外提供读写服务(通过 Ask/Moved 重定向机制),实现了真正意义上的平滑扩容,而不需要像一致性哈希那样去重新计算复杂的环上顺时针跨度。
一目了然的集群状态:哪个节点负责哪些数据,清清楚楚。如果某台机器挂了,只需要把她负责的槽对应的从库(Replica)提为主库,或者把槽转移走即可,极易维护。
为什么 redis 只有 16384 个槽?
Redis 的作者 Antirez 曾经在 GitHub 上亲自回答过这个问题。总结起来,选择*16384(即 16K,即 $2^{14}$) 而不是更直观的 65536(即 64K,即 $2^{16}$),完全是在网络心跳开销、集群规模上限以及压缩效率之间做出的顶级工程权衡(Trade-off)。
具体原因之一:心跳包(Ping/Pong)的带宽开销太大。Redis Cluster 中的每个节点都需要定期向其他节点发送 PING 消息,以便检测对方是否在线。为了让其他节点知道 “我这个节点目前负责哪些槽”,PING 消息的报文头(Header)里必须附带一个位图(Bitmap),用每一位(bit)来代表自己是否负责对应的槽。如果是 16384 个槽:位图大小为:$16384 \div 8 \text{ bit} = 2048 \text{ 字节} = \mathbf{2\text{ KB}}$。如果是 65536 个槽:位图大小为:$65536 \div 8 \text{ bit} = 8192 \text{ 字节} = \mathbf{8\text{ KB}}$。在 Redis 集群中,心跳是非常频繁的(每秒都会有很多 PING/PONG 发送)。如果一个拥有上百个节点的集群,每次心跳仅槽位信息就要额外多吃掉 6 KB 的带宽,整个集群的内网网络带宽会被这些无意义的心跳报文严重榨干。因此,作者为了极致的吞吐量,必须精简心跳包的大小。
具体原因之二:集群规模的现实制约——节点数不可能超过 1000 个。Redis 作者认为,Redis Cluster 的集群规模绝对不建议超过 1000 个 Master 节点。如果节点数超过 1000 个,节点间八卦协议(Gossip)的网络广播就会引发“网络风暴”,集群内部自己就把自己卡死了。在节点数不超过 1000 个的前提下,16384 个槽已经绰绰有余了——即使有 1000 个节点,平均每个节点也能分到 16 个槽,完全能够满足极细粒度的数据分片和动态扩容需求。所以,盲目把槽扩大到 65536 没有任何实际工程意义。
具体原因之三:底层压缩神技——16384 在特定场景下压缩率极高。Redis 在传输槽位图时,并不是一成不变的,在很多时候会对位图进行压缩后再传输。当槽数量为 16384 时:由于集群节点数一般比较少,每个节点分到的槽通常是连续且成块的(比如 NodeA 负责 0-5000)。这种高连续性的位图,在使用位图压缩算法时,其压缩比恐怖到惊人(一堆连续的 1 可以被极度压缩),2 KB 的数据常常能被压缩到几十个字节。如果强行换成 65536 个槽,由于槽位的基数变大,在分片、迁移等过程中,位图中的零散度会成倍上升,导致压缩率大打折扣。
总结起来就是:哈希取余属于早期的静态分片方案,简单但没有弹性,不具备动态扩缩容能力; 一致性哈希通过 ‘哈希环’ 巧妙地解决了扩容时数据全量失效的痛点,但在工程落地时,其被动的顺时针路由导致扩容粒度较粗、不易精细化控制; 哈希槽算法则是现代分布式存储(如 Redis Cluster)的工业级标准,它引入了 ‘虚拟槽’ 这一中间层,彻底解耦了数据与节点的直接映射,让大规模集群的平滑扩容、在线数据迁移变得极其可控与稳定。
三主三从集群的搭建 第一步:一键创建宿主机目录与配置文件。 为了不重蹈 “挂载成文件夹” 的覆辙,我们直接采用挂载整个配置目录的高级规范。同时,由于有 6 个节点,手写 6 个配置文件太低效了。在宿主机终端直接复制并运行以下命令,它会自动帮你格式化创建出 6 个节点所需的文件夹和 redis.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 $ docker network --help $ docker network create redis-net $ docker network ls NETWORK ID NAME DRIVER SCOPE 2133c9ab0010 bridge bridge local d3385e8df5e5 host host local c788000c6843 none null local b165ac73c636 redis-net bridge local $ for port in $(seq 6381 6386); do \ mkdir -p /opt/apps/redis-cluster/${port} /conf /opt/apps/redis-cluster/${port} /data; \ cat <<EOF > /opt/apps/redis-cluster/${port}/conf/redis.conf # 业务端口 port ${port} # 允许任意 IP 访问 bind 0.0.0.0 # 关闭保护模式 protected-mode no # 容器环境必须为 no daemonize no # 开启 AOF 持久化 appendonly yes # 设置密码 requirepass 123456 # 集群节点间访问也需要密码 masterauth 123456 # === 核心集群配置 === # 开启集群模式 cluster-enabled yes # 集群配置文件名称(容器会自动创建并维护) cluster-config-file nodes.conf # 节点超时时间(毫秒) cluster-node-timeout 5000 # 宣告宿主机的物理 IP(关键!防止外界无法路由) cluster-announce-ip 192.168.1.8 # 宣告业务端口 cluster-announce-port ${port} # 宣告集群总线端口(业务端口 + 10000) cluster-announce-bus-port 1${port} EOF done
第二步:一键启动 6 个 Redis 容器节点 。目录和配置都生成好了,接下来我们用一个循环,把 6 个容器齐刷刷地全部拉起来。在宿主机终端继续执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ for port in $(seq 6381 6386); do \ docker run -d \ --name redis-${port} \ --net redis-net \ -p ${port} :${port} \ -p 1${port} :1${port} \ --privileged=true \ -v /opt/apps/redis-cluster/${port} /conf:/etc/redis \ -v /opt/apps/redis-cluster/${port} /data:/data \ --restart=unless-stopped \ redis:8.8.0 \ redis-server /etc/redis/redis.conf; \ done $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 008a099e2434 redis:8.8.0 "docker-entrypoint.s…" 13 seconds ago Up 11 seconds 0.0.0.0:6386->6386/tcp, [::]:6386->6386/tcp, 6379/tcp, 0.0.0.0:16386->16386/tcp, [::]:16386->16386/tcp redis-6386 85f69e3bf668 redis:8.8.0 "docker-entrypoint.s…" 13 seconds ago Up 12 seconds 0.0.0.0:6385->6385/tcp, [::]:6385->6385/tcp, 6379/tcp, 0.0.0.0:16385->16385/tcp, [::]:16385->16385/tcp redis-6385 9f382490b3ca redis:8.8.0 "docker-entrypoint.s…" 14 seconds ago Up 13 seconds 0.0.0.0:6384->6384/tcp, [::]:6384->6384/tcp, 6379/tcp, 0.0.0.0:16384->16384/tcp, [::]:16384->16384/tcp redis-6384 9079ded11550 redis:8.8.0 "docker-entrypoint.s…" 15 seconds ago Up 13 seconds 0.0.0.0:6383->6383/tcp, [::]:6383->6383/tcp, 6379/tcp, 0.0.0.0:16383->16383/tcp, [::]:16383->16383/tcp redis-6383 a5478222791f redis:8.8.0 "docker-entrypoint.s…" 15 seconds ago Up 14 seconds 0.0.0.0:6382->6382/tcp, [::]:6382->6382/tcp, 6379/tcp, 0.0.0.0:16382->16382/tcp, [::]:16382->16382/tcp redis-6382 05e4dabe8995 redis:8.8.0 "docker-entrypoint.s…" 16 seconds ago Up 15 seconds 0.0.0.0:6381->6381/tcp, [::]:6381->6381/tcp, 6379/tcp, 0.0.0.0:16381->16381/tcp, [::]:16381->16381/tcp redis-6381
第三步:握手组建集群(分配 3主3从)。 现在 6 个节点只是孤独运行的单机,我们需要把它们召集起来,指定 3主3从,并让它们自动瓜分 16384 个哈希槽。由于我们配置了密码,所以在创建集群时,必须带上 -a 参数。随便挑一个容器(比如连入 redis-6381)去执行集群初始化命令。直接复制运行下面这行:
1 2 3 4 5 6 $ docker exec -it redis-6381 redis-cli -a 123456 --cluster create \ 192.168.1.8:6381 192.168.1.8:6382 192.168.1.8:6383 \ 192.168.1.8:6384 192.168.1.8:6385 192.168.1.8:6386 \ --cluster-replicas 1
第四步:集群验证 。集群和普通单机不一样,测试时需要加上 -c 参数(代表集群模式,开启自动槽位重定向)。我们可以进入 6381 节点塞入一个数据,看看它会不会被自动路由到其他 Master 节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 $ docker exec -it redis-6381 redis-cli -c -a 123456 -p 6381 127.0.0.1:6381> set k1 "v1" -> Redirected to slot [12706] located at 192.168.1.8:6383 OK 192.168.1.8:6383> cluster info cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6 cluster_size:3 cluster_current_epoch:6 cluster_my_epoch:3 cluster_stats_messages_ping_sent:444 cluster_stats_messages_pong_sent:431 cluster_stats_messages_meet_sent:1 cluster_stats_messages_sent:876 cluster_stats_messages_ping_received:431 cluster_stats_messages_pong_received:448 cluster_stats_messages_received:879 total_cluster_links_buffer_limit_exceeded:0 cluster_slot_migration_active_tasks:0 cluster_slot_migration_active_trim_running:0 cluster_slot_migration_active_trim_current_job_keys:0 cluster_slot_migration_active_trim_current_job_trimmed:0 cluster_slot_migration_stats_active_trim_started:0 cluster_slot_migration_stats_active_trim_completed:0 cluster_slot_migration_stats_active_trim_cancelled:0 192.168.1.8:6383> cluster nodes 424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 master - 0 1780196534000 2 connected 5461-10922 6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 master - 0 1780196534868 1 connected 0-5460 8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 myself,master - 0 0 3 connected 10923-16383 aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780196535000 3 connected cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 slave 6c37172dde90d68cde4605cb25733072c708ed3e 0 1780196534000 1 connected 5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780196535874 2 connected $$ redis-cli --cluster check 192.168.1.8:6381 -a 123456 192.168.1.8:6381 (6c37172d...) -> 0 keys | 5461 slots | 1 slaves. 192.168.1.8:6382 (424a976b...) -> 0 keys | 5462 slots | 1 slaves. 192.168.1.8:6383 (8ec9e604...) -> 1 keys | 5461 slots | 1 slaves. [OK] 1 keys in 3 masters. 0.00 keys per slot on average. >>> Performing Cluster Check (using node 192.168.1.8:6381) M: 6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381 slots:[0-5460] (5461 slots) master 1 additional replica(s) M: 424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382 slots:[5461-10922] (5462 slots) master 1 additional replica(s) M: 8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383 slots:[10923-16383] (5461 slots) master 1 additional replica(s) S: 5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386 slots: (0 slots) slave replicates 424a976bfbf96a0bdae0012157449ea6907f9d17 S: cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385 slots: (0 slots) slave replicates 6c37172dde90d68cde4605cb25733072c708ed3e S: aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384 slots: (0 slots) slave replicates 8ec9e60429b52db4a982502839c8a52c01015905 [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered.
目前节点的状态图:
1 2 3 master 6381 {0-5460} -------- slave 6385 master 6382 {5461-10922} -------- slave 6386 master 6383 {10923-16383} -------- slave 6384
主从容错的演示 第一:验证停掉 master 6381,看看 slave 6385 是否会升级为新的 master。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $ docker ps $ docker stop redis-6381 $ docker exec -it redis-6382 redis-cli -c -a 123456 -p 6382 127.0.0.1:6382> cluster nodes 8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 master - 0 1780197842111 3 connected 10923-16383 424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 myself,master - 0 0 2 connected 5461-10922 5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780197843126 2 connected 6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 master,fail - 1780197660355 1780197657783 1 disconnected aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780197842000 3 connected cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 master - 0 1780197841604 7 connected 0-5460 master 6385 {0-5460} -------- x master 6382 {5461-10922} -------- slave 6386 master 6383 {10923-16383} -------- slave 6384
第二:验证重新启动 6381,看看新集群的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $ docker start redis-6381 $ docker exec -it redis-6382 redis-cli -c -a 123456 -p 6382 127.0.0.1:6382> cluster nodes 8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 master - 0 1780198267509 3 connected 10923-16383 424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 myself,master - 0 0 2 connected 5461-10922 5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780198267509 2 connected 6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 slave cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 0 1780198267812 7 connected aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780198266601 3 connected cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 master - 0 1780198266803 7 connected 0-5460 master 6385 {0-5460} -------- slave 6381 master 6382 {5461-10922} -------- slave 6386 master 6383 {10923-16383} -------- slave 6384
集群扩容演示 在原6台节点集群的基础上,增加 redis-6387、redis-6388 两个节点,redis-6387 作为新的主节点(获取槽位),redis-6388 作为 redis-6387 的从节点。
第一步:在宿主机创建新节点目录并启动容器。 首先,我们需要在宿主机(192.168.1.8)上把 6387 和 6388 的家当准备好,并把它们拉起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 $ for port in 6387 6388; do \ mkdir -p /opt/apps/redis-cluster/${port} /conf /opt/apps/redis-cluster/${port} /data; \ cat <<EOF > /opt/apps/redis-cluster/${port}/conf/redis.conf port ${port} bind 0.0.0.0 protected-mode no daemonize no appendonly yes requirepass 123456 masterauth 123456 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 cluster-announce-ip 192.168.1.8 cluster-announce-port ${port} cluster-announce-bus-port 1${port} EOF docker run -d \ --name redis-${port} \ --net redis-net \ -p ${port} :${port} \ -p 1${port} :1${port} \ --privileged=true \ -v /opt/apps/redis-cluster/${port} /conf:/etc/redis \ -v /opt/apps/redis-cluster/${port} /data:/data \ --restart=unless-stopped \ redis:8.8.0 \ redis-server /etc/redis/redis.conf; \ done
第二步:将新节点作为无槽空节点加入集群。 新容器启动后,它们还只是孤立的单机,我们需要使用 redis-cli –cluster add-node 命令把它们引荐给集群。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ docker exec -it redis-6381 redis-cli -a 123456 --cluster add-node 192.168.1.8:6387 192.168.1.8:6381 $ docker exec -it redis-6381 redis-cli -a 123456 --cluster add-node 192.168.1.8:6388 192.168.1.8:6381 --cluster-slave --cluster-master-id cb71175feac4f019edac69031c45e42d63c3e197 $ docker exec -it redis-6382 redis-cli -c -a 123456 -p 6382 127.0.0.1:6382> cluster nodes master 6385 {0-5460} -------- slave 6381 master 6382 {5461-10922} -------- slave 6386 master 6383 {10923-16383} -------- slave 6384 master 6387 {} -------- slave 6388
第三步:在线重新分配哈希槽(Reshard)给 6387 。们现在要把原本 3 个老 Master 治下的 16384 个槽,匀出一部分给 6387。原本 3 主时代,平均每个主负责 $16384 \div 3 = 5461$ 个槽。现在变为了 4 主,平均每个主应该负责 $16384 \div 4 = \mathbf{4096}$ 个槽。也就是说,新主节点 6387 应该从老的三台主节点身上一共抽调 4096 个槽过来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 $ docker exec -it redis-6381 redis-cli -a 123456 -p 6381 cluster nodes | grep 6387 $ docker exec -it redis-6381 redis-cli -a 123456 --cluster reshard 192.168.1.8:6381 $ docker exec -it redis-6381 redis-cli -c -a 123456 -p 6381 127.0.0.1:6381> cluster nodes 424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 master - 0 1780201520553 2 connected 6827-10922 6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 slave cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 0 1780201520000 7 connected 8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 myself,master - 0 0 3 connected 12288-16383 65600984ab36afd9a4106cd6c668547e9e5fa75f 192.168.1.8:6388@16388 slave cb71175feac4f019edac69031c45e42d63c3e197 0 1780201518710 9 connected aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780201520552 3 connected cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 master - 0 1780201518508 7 connected 1365-5460 5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780201520956 2 connected cb71175feac4f019edac69031c45e42d63c3e197 192.168.1.8:6387@16387 master - 0 1780201520000 9 connected 0-1364 5461-6826 10923-12287 master 6385 {1365-5460} -------- slave 6381 master 6382 {6827-10922} -------- slave 6386 master 6383 {12288-16383} -------- slave 6384 master 6387 {0-1364 5461-6826 10923-12287} -------- slave 6388
集群缩容演示 现在集群的压力没有这么大了,需要缩容,删除 redis-6387 和 redis-6388 节点。将多出的槽位均分到之前的节点上。
第一步:把 6387 上的槽位吐出来 。先把 6387 的槽一次性全倒给 6385【接收槽位的必须是主节点】。此时,6387 瞬间沦为空壳(0槽),它已经达到了可以被安全删除的标准。而 6385 此时暴涨到了 $4096 + 4096 = 8192$ 个槽,处于严重的“消化不良”状态。
1 2 3 4 5 6 7 8 9 10 11 $ docker exec -it redis-6385 redis-cli -a 123456 --cluster reshard 192.168.1.8:6385
第二步:召唤一键平衡神技(Rebalance) 。
现在 6387 已经空了,我们先把 6387 和 6388 删掉(避免它们参与后续的槽位瓜分)。
1 2 3 4 5 $ docker exec -it redis-6381 redis-cli -a 123456 --cluster del-node 192.168.1.8:6381 65600984ab36afd9a4106cd6c668547e9e5fa75f $ docker exec -it redis-6381 redis-cli -a 123456 --cluster del-node 192.168.1.8:6381 cb71175feac4f019edac69031c45e42d63c3e197
执行自动均衡命令。此时集群里只剩下原本的老三台 Master 和老三台 Slave了。我们不需要再苦哈哈地去算每台机器分多少,直接掏出 Redis Cluster 的大杀器——rebalance:Redis 收到命令后,会扫视当前集群中所有健康的 Master 节点。它发现 6385 一个人胖成了球,而 6382 和 6383 瘦得可怜(默认的倾斜程度阈值为2%)。于是算法在后台会自动、精准地将 6385 身上多出来的槽位均分给 6382 和 6383,直到大家的槽位重新恢复成完美的 5461 或 5462(16384/3)。
1 2 3 4 5 6 7 8 9 10 11 $ docker exec -it redis-6381 redis-cli -a 123456 --cluster rebalance 192.168.1.8:6381 --cluster-threshold 1 127.0.0.1:6381> cluster nodes aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780204082000 11 connected 8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 master - 0 1780204083000 11 connected 0-2731 13654-16383 5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780204082000 12 connected 424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 master - 0 1780204083575 12 connected 2732-5461 8192-10922 cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 master - 0 1780204083979 10 connected 5462-8191 10923-13653 6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 myself,slave cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 0 0 10 connected
在实际生产环境中,由于肉眼计算和手动复制 40 位的 Node ID 极易出错(比如不小心拷错了一个字母,或者数字算错了一位导致槽位没对齐),手动一次性迁移 + 官方 rebalance 自动化均分 的组合拳,能够将人为操作失误的概率降到最低。